Skip to content

sp_BlitzFirst: add Top 5 server/login/app breakdown to High Number of Connections#3921

Merged
BrentOzar merged 3 commits into
devfrom
3903_sp_BlitzFirst_connections
Apr 15, 2026
Merged

sp_BlitzFirst: add Top 5 server/login/app breakdown to High Number of Connections#3921
BrentOzar merged 3 commits into
devfrom
3903_sp_BlitzFirst_connections

Conversation

@BrentOzar
Copy link
Copy Markdown
Member

@BrentOzar BrentOzar commented Apr 15, 2026

Closes #3903.

Summary

When CheckID 49 (High Number of Connections) fires, the Details column now includes three ranked sections so operators can see where the connection pressure is concentrated:

  • Top 5 Servers — by sys.dm_exec_sessions.host_name
  • Top 5 Logins — by login_name
  • Top 5 Apps — by program_name

Each line shows the connection count plus the most-recent and oldest query-finish ages for that group:

AppServer14 - 106 connections, most recent query finished 3 seconds ago, oldest query finished 4 hours 15 minutes ago
WebServer11 - 98 connections, most recent query finished 14 seconds ago, oldest query finished 3 hours 15 minutes ago

Implementation notes

  • DMVs read once. Connection + session attributes land in a TABLE variable that's aggregated three different ways. No correlated triple-scan.
  • Gated on the alert firing. Total connection count is computed into @TotalConnections first; the breakdown work only runs when @TotalConnections > @max_worker_threads. No extra cost on healthy servers.
  • Epoch sentinel handled. sys.dm_exec_sessions.last_request_end_time defaults to 1900-01-01 for sessions that have never run a request. That gets NULLIF'd so the row renders as "unknown" rather than "45000 days ago".
  • Unknown buckets. NULL/empty host_name, login_name, program_name collapse into (unknown host) / (unknown login) / (unknown app) so internal or anonymous connections don't drop out of the count.
  • Entity-safe concatenation. Uses .value(N'.[1]', N'NVARCHAR(MAX)') — the same pattern already used elsewhere in sp_BlitzFirst — so program_name values containing <, >, or & don't truncate the output.
  • Deterministic ordering. ORDER BY ConnectionCount DESC, ConnectionGroup — no ties cause random row shuffling.

Alternative to this PR: #3904 (same feature, different approach). This version avoids the scalar-aggregate + CROSS APPLY compile issue flagged in review there and collapses the age-formatting CASE from 12 copies to 6.

Test plan

  • On a server with < @max_worker_threads connections, confirm the check is silent (no row in #BlitzFirstResults).
  • Flood the server with connections (e.g. ostress -n3000 or a load generator against a VM with low core count so @max_worker_threads is low), confirm the alert fires and the three Top-5 sections populate.
  • Verify a mix of connection sources: different hosts, logins, and program_name values — confirm counts sum correctly to the total.
  • Confirm connections from a brand-new session (no request ever run) show "unknown" rather than a huge day count.
  • Confirm an app whose program_name contains <, >, or & still appears in the Top 5 Apps list (exercises the .[1] XPath fix).
  • Confirm the check is skipped entirely on non-64-bit Windows / Azure SQL DB (existing guard unchanged).

🤖 Generated with Claude Code

BrentOzar and others added 2 commits April 15, 2026 12:24
… Connections (#3903)

When CheckID 49 fires, the Details column now includes three ranked sections
(Top 5 Servers, Top 5 Logins, Top 5 Apps) showing connection counts plus the
most-recent and oldest query finish ages for each. Operators can quickly spot
whether connection pressure is concentrated in one app server or login vs.
spread across the fleet.

Implementation notes:
* Loads sys.dm_exec_connections joined to sys.dm_exec_sessions into a table
  variable once, then aggregates it three ways so the DMVs are only read once.
* Treats sys.dm_exec_sessions.last_request_end_time = 1900-01-01 (the sentinel
  for sessions that have never run a request) as NULL so it renders as
  "unknown" rather than "45000 days ago".
* Pre-computes the total connection count into a variable and gates the
  breakdown work behind an explicit IF so we only do the extra aggregation
  when the alert actually fires.
* Bumps sp_BlitzFirst version to 8.33 / 20260415.

Closes #3903.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Version and VersionDate on line 50 are maintained by the build process;
hand-edits there get overwritten.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@BrentOzar BrentOzar marked this pull request as ready for review April 15, 2026 16:50
Copilot AI review requested due to automatic review settings April 15, 2026 16:50
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances sp_BlitzFirst CheckID 49 (“High Number of Connections”) so that when the alert triggers, the Details output includes a Top-5 breakdown of connection sources (by host, login, and application) with recency/age context to help operators quickly identify where connection pressure is concentrated.

Changes:

  • Computes total connection count first and only generates breakdown details when the alert threshold is exceeded.
  • Captures connection/session attributes into a single in-memory structure and aggregates it three ways (Top 5 Servers / Logins / Apps).
  • Appends the Top-5 breakdown sections to the existing CheckID 49 Details text.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread sp_BlitzFirst.sql Outdated
Comment on lines +2734 to +2735
FOR XML PATH(''), TYPE
).value('text()[1]', 'nvarchar(max)'), 1, LEN(@LineFeed), N'');
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML concatenation uses .value('text()[1]', 'nvarchar(max)'), which only returns the first text node (so you’ll only get the first Top-5 row in the output). Use .value('.', 'nvarchar(max)') (or the existing pattern in this proc: .value(N'.[1]', N'NVARCHAR(MAX)')) to return the full concatenated string.

Copilot uses AI. Check for mistakes.
Comment thread sp_BlitzFirst.sql Outdated
Comment on lines +2771 to +2772
FOR XML PATH(''), TYPE
).value('text()[1]', 'nvarchar(max)'), 1, LEN(@LineFeed), N'');
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above: .value('text()[1]', 'nvarchar(max)') will only return the first concatenated row for Top Logins. Switch to .value('.', 'nvarchar(max)') / .value(N'.[1]', N'NVARCHAR(MAX)') so all Top-5 lines are included.

Copilot uses AI. Check for mistakes.
Comment thread sp_BlitzFirst.sql Outdated
Comment on lines +2808 to +2809
FOR XML PATH(''), TYPE
).value('text()[1]', 'nvarchar(max)'), 1, LEN(@LineFeed), N'');
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same XML extraction issue here: .value('text()[1]', 'nvarchar(max)') returns only the first text node, so Top Apps will likely show only one line. Use .value('.', 'nvarchar(max)') (or .value(N'.[1]', N'NVARCHAR(MAX)')) to return the full concatenation.

Copilot uses AI. Check for mistakes.
Comment thread sp_BlitzFirst.sql
Comment thread sp_BlitzFirst.sql Outdated
Comment on lines +2821 to +2823
+ @LineFeed + @LineFeed + 'Top 5 Servers:' + ISNULL(@TopServers, @LineFeed + '(none)')
+ @LineFeed + @LineFeed + 'Top 5 Logins:' + ISNULL(@TopLogins, @LineFeed + '(none)')
+ @LineFeed + @LineFeed + 'Top 5 Apps:' + ISNULL(@TopApps, @LineFeed + '(none)')
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TopServers/@TopLogins/@TopApps are built with STUFF(..., 1, LEN(@LineFeed), N''), so they do not start with a line break. In the final Details string you concatenate 'Top 5 X:' + ISNULL(@TopX, ...) (no @LineFeed after the colon), which will render the first entry on the same line as the header. Add a line break after each header (or keep the leading line break in @TopX).

Suggested change
+ @LineFeed + @LineFeed + 'Top 5 Servers:' + ISNULL(@TopServers, @LineFeed + '(none)')
+ @LineFeed + @LineFeed + 'Top 5 Logins:' + ISNULL(@TopLogins, @LineFeed + '(none)')
+ @LineFeed + @LineFeed + 'Top 5 Apps:' + ISNULL(@TopApps, @LineFeed + '(none)')
+ @LineFeed + @LineFeed + 'Top 5 Servers:' + @LineFeed + ISNULL(@TopServers, '(none)')
+ @LineFeed + @LineFeed + 'Top 5 Logins:' + @LineFeed + ISNULL(@TopLogins, '(none)')
+ @LineFeed + @LineFeed + 'Top 5 Apps:' + @LineFeed + ISNULL(@TopApps, '(none)')

Copilot uses AI. Check for mistakes.
… output

Two fixes based on Copilot review feedback on PR #3921:

* Switch .value('text()[1]', 'nvarchar(max)') to .value(N'.[1]', N'NVARCHAR(MAX)')
  on all three Top-5 STUFF/FOR XML blocks. text()[1] returns only the first
  text node; when a program_name contains <, >, or & it gets entity-escaped
  and breaks the concatenation into multiple text nodes, so only the first
  row would appear. The new form matches the existing pattern used elsewhere
  in sp_BlitzFirst (see line 2487).

* Insert @linefeed between each "Top 5 X:" header and the first row. The
  Top-5 strings are built with STUFF(..., 1, LEN(@linefeed), N'') so they
  don't start with a line break, which was running the first entry onto
  the header line.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@BrentOzar
Copy link
Copy Markdown
Member Author

Thanks @copilot. Addressed in dc10aef:

  • XPath comments (lines 2735/2772/2809): Valid catch. Switched all three .value('text()[1]', 'nvarchar(max)') to .value(N'.[1]', N'NVARCHAR(MAX)') — matches the existing FRK pattern at line 2487 and protects against program_name values containing <, >, or &, which get entity-escaped into separate text nodes.
  • Missing line break after headers (line 2823): Valid catch. Applied the suggested fix.
  • Version bump (line 2671): Not applying. The @Version / @VersionDate line around line 50 of each sp is maintained by the build process; the earlier commit cd914b7 reverted my original bump for that reason. I've dropped the version-bump line from the PR description so it's no longer misleading.

@BrentOzar BrentOzar added this to the 2026-07 Release milestone Apr 15, 2026
@BrentOzar BrentOzar merged commit 937e05a into dev Apr 15, 2026
1 check passed
@BrentOzar BrentOzar deleted the 3903_sp_BlitzFirst_connections branch April 15, 2026 17:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sp_BlitzFirst: add status line for number of connections

2 participants